Skip to content

feat(analytics): Platform metric - #165

Merged
yash-pouranik merged 4 commits into
mainfrom
feat/metrics-stack
May 11, 2026
Merged

feat(analytics): Platform metric#165
yash-pouranik merged 4 commits into
mainfrom
feat/metrics-stack

Conversation

@yash-pouranik

@yash-pouranik yash-pouranik commented May 11, 2026

Copy link
Copy Markdown
Member

Overview

This PR introduces a comprehensive, self-hosted Startup Metrics Stack to monitor platform health, track developer activation, and measure product retention without relying on external third-party dependencies.

The system is built on asynchronous event logging (emitEvent) and daily BullMQ rollups to ensure absolute zero performance penalty on critical path API requests.

Key Changes

1. Core Data Models & Queues (packages/common)

  • Added PlatformEvent model (2-yr TTL) for discrete actions (e.g., signup, API success).
  • Added DeveloperActivity model for daily time-series feature usage aggregation.
  • Created activityRollupQueue (cron job running daily at 00:05 UTC) to compress raw logs into cohort/retention data.
  • Created reliabilityAlertQueue (cron job running every 5 minutes) to proactively detect project error rate spikes (>5% in 15 min window) and log reliability_spike events.

2. Activation Funnel Instrumentation

  • Instrumented auth.controller (signup_completed, email_verified).
  • Instrumented project.controller (project_created, collection_created).
  • Instrumented api_usage middleware using a Redis atomic flag (NX) to guarantee first_api_success is emitted exactly once per project without DB overhead.

3. Operator Admin Dashboard

  • Added 7 new endpoints under dashboard-api (/api/admin/metrics/*) protected by JWT + isAdmin guards.
  • Built the new AdminMetrics.jsx operator page in the React dashboard featuring:
    • Global Platform Overview & 7-day North Star metric.
    • Complete Activation Funnel waterfall.
    • Interactive D1/D7/D30 Retention Cohorts (filterable by signup month).
    • Feature Usage breakdown (API, Storage, Mails, Webhooks).
    • Proactive "Churn Signals" table with owner email resolution.

4. Per-Developer UX

  • Exposed personal analytics endpoints (/api/analytics/funnel, /api/analytics/engagement).
  • Added the DeveloperMetrics component to the main developer dashboard to show their personal Activation Status and 30-Day Activity at a glance.

Testing & Verification

  • apps/public-api and apps/dashboard-api test suites fully passing (100% success rate on existing auth/RLS tests).
  • React frontend linting strict compliance verified (npm run lint passing).
  • Validated transactional boundaries during collection/project creation to ensure events only emit on successful DB commits.
    ```Ran command: clear

Summary by CodeRabbit

  • New Features
    • Event tracking system for monitoring key user actions and milestones
    • Developer metrics dashboard displaying activation status and 30-day activity summary
    • Admin metrics panel with platform overview, activation funnel, retention cohorts, feature usage, reliability tracking, and project performance insights
    • Automated activity rollup and reliability alert monitoring in the background

Review Change Stack

…unnel, and admin metrics dashboard

This commit introduces the Startup Metrics Stack to monitor platform health and track developer activation.

Key Changes:
- Core Data: Added PlatformEvent and DeveloperActivity models.
- BullMQ Jobs: Implemented daily activityRollupQueue and 5-min reliabilityAlertQueue to track error rate spikes.
- Activation Funnel: Instrumented auth, project creation, and API usage to emit pipeline events.
- Admin Dashboard: Added 7 new protected metrics APIs and built the AdminMetrics UI for operators.
- Developer UI: Added DeveloperMetrics component to show personal activation status and 30-day activity.
@coderabbitai

coderabbitai Bot commented May 11, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@Copilot has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 45 minutes and 49 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d1e01962-febf-4e13-85de-a54e85af9b30

📥 Commits

Reviewing files that changed from the base of the PR and between 543c202 and ac4eef0.

📒 Files selected for processing (10)
  • apps/dashboard-api/src/controllers/admin.metrics.controller.js
  • apps/dashboard-api/src/controllers/analytics.controller.js
  • apps/dashboard-api/src/controllers/events.controller.js
  • apps/dashboard-api/src/routes/events.js
  • apps/public-api/src/app.js
  • apps/public-api/src/middlewares/api_usage.js
  • apps/public-api/src/utils/emitEvent.js
  • apps/web-dashboard/src/components/Dashboard/DeveloperMetrics.jsx
  • packages/common/src/queues/activityRollupQueue.js
  • packages/common/src/queues/reliabilityAlertQueue.js
📝 Walkthrough

Walkthrough

This PR introduces a comprehensive event tracking and analytics platform. It adds PlatformEvent and DeveloperActivity models, background workers for activity rollup and reliability monitoring, event emission across auth/project/API flows, analytics endpoints for users and admins, and a web-based admin dashboard displaying platform-wide metrics including funnel completion, retention cohorts, feature usage, and churn signals.

Changes

Platform Event Tracking and Analytics Infrastructure

Layer / File(s) Summary
Data Models
packages/common/src/models/PlatformEvent.js, packages/common/src/models/DeveloperActivity.js
PlatformEvent schema stores developerId, projectId, event name, free-form properties, and timestamp with TTL/compound indexes for funnel queries; DeveloperActivity rollup schema aggregates per-developer daily activity counters and active project IDs.
Event Emission Utilities
apps/dashboard-api/src/utils/emitEvent.js, apps/public-api/src/utils/emitEvent.js
Fire-and-forget emitEvent(developerId, event, properties, projectId) utilities scheduled via setImmediate to create PlatformEvent records without blocking callers or throwing on DB errors.
Auth Event Emissions
apps/dashboard-api/src/controllers/auth.controller.js
GitHub OAuth, email registration, and OTP verification now emit signup_completed and email_verified events with authentication method in properties.
User Action Events
apps/dashboard-api/src/controllers/project.controller.js, apps/public-api/src/middlewares/api_usage.js
project_created and collection_created events emitted after provisioning; first_api_success emitted on first 2xx API response per project (Redis NX guard).
Frontend Event Tracking
apps/dashboard-api/src/controllers/events.controller.js, apps/dashboard-api/src/routes/events.js
New POST /api/events/track endpoint validates and normalizes event names against ALLOWED_FRONTEND_EVENTS allowlist, emits events tagged with _source: 'frontend'.
Background Workers
packages/common/src/queues/activityRollupQueue.js, packages/common/src/queues/reliabilityAlertQueue.js
BullMQ-based workers: daily runRollup() aggregates Logs by developer for DeveloperActivity upserts; 5-minute runReliabilityCheck() detects ≥5% error-rate spikes and emits reliability_spike events.
Analytics Controllers
apps/dashboard-api/src/controllers/analytics.controller.js, apps/dashboard-api/src/controllers/admin.metrics.controller.js
User endpoints (getActivationFunnel, getRetention, getEngagement, getNorthStar) compute per-developer metrics from PlatformEvent and DeveloperActivity; admin endpoints compute platform-wide overview, funnel, retention cohorts, feature usage, top projects, and churn signals.
API Routes
apps/dashboard-api/src/routes/admin.metrics.js, apps/dashboard-api/src/routes/analytics.js, apps/dashboard-api/src/routes/events.js, apps/dashboard-api/src/app.js, apps/public-api/src/app.js
Register /api/events, /api/admin/metrics/*, and new /api/analytics/* routes with auth/email verification guards; start background workers in public-api.
Admin Dashboard
apps/web-dashboard/src/pages/AdminMetrics.jsx, apps/web-dashboard/src/index.css, apps/web-dashboard/src/App.jsx
New AdminMetrics page fetches 6 metric endpoints in parallel, displays overview stats, activation funnel, retention cohorts (selectable by month), feature usage, reliability (with error-rate-based accent coloring), and top projects/churn tables. Comprehensive CSS for stat cards, funnel bars, tables, and controls.
User Dashboard Metrics
apps/web-dashboard/src/components/Dashboard/DeveloperMetrics.jsx, apps/web-dashboard/src/pages/Dashboard.jsx
New DeveloperMetrics component displays per-developer activation progress percentage and 30-day activity stats (API calls, mail sent, storage uploads, webhooks) in the dashboard sidebar.
Package Exports
packages/common/src/index.js, packages/common/src/models/index.js
Re-export new models (PlatformEvent, DeveloperActivity) and queue utilities (activityRollupQueue, reliabilityAlertQueue, scheduler/worker functions).
Test Mocks
apps/dashboard-api/src/__tests__/auth.controller.test.js
Mock PlatformEvent.create for controller test coverage.

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • geturbackend/urBackend#151: Both PRs modify ApiAnalytics model usage and apps/public-api/src/middlewares/api_usage.js for reliability tracking.
  • geturbackend/urBackend#140: Both PRs use ApiAnalytics aggregation in dashboard controllers for latency and error-rate metrics.
  • geturbackend/urBackend#84: Both PRs extend packages/common/src/index.js exports for worker utilities and queues.

Suggested labels

feature, backend

Poem

🐰 Events hop through the system with grace,
Platform metrics now have their own place,
Funnels and dashboards paint the full story,
From signup to churn, we measure in glory,
Background workers toil without a trace!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 73.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The PR title is vague and incomplete. 'Platform metric' (singular, lowercase) lacks specificity and doesn't convey the actual scope of changes, which involve comprehensive metrics infrastructure, event logging, admin dashboards, and multiple feature additions. Revise the title to be more descriptive, such as 'feat: Add platform metrics stack with event logging and admin dashboard' or 'feat(metrics): Implement developer activation and platform health tracking'. This better summarizes the PR's substantial feature additions.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/metrics-stack

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
apps/dashboard-api/src/controllers/auth.controller.js (1)

161-189: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing email_verified emission on GitHub email-merge path.

When an existing local account is reconciled by email and we force isVerified = true (Lines 161–169), no email_verified event is emitted. If that account was previously unverified, this branch flips it to verified without funneling the transition into the activation/funnel pipeline — analytics will under-count verifications. Consider emitting email_verified here when the prior state was false.

🛡️ Proposed fix
     developer = await Developer.findOne({ email: profile.email }).select('+password +refreshToken');
     if (developer) {
+        const wasVerifiedBefore = developer.isVerified === true;
         developer.githubId = profile.githubId;
         developer.githubUsername = profile.githubUsername;
         developer.avatarUrl = profile.avatarUrl;
         developer.isVerified = true;
         await developer.save();
+        if (!wasVerifiedBefore) {
+            emitEvent(developer._id, 'email_verified', { method: 'github' });
+        }
         return developer;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/dashboard-api/src/controllers/auth.controller.js` around lines 161 -
189, When reconciling an existing Developer in the GitHub flow (the branch that
finds Developer via Developer.findOne and sets
developer.githubId/githubUsername/avatarUrl/isVerified), check the previous
isVerified value and, if it was false, call emitEvent(developer._id,
'email_verified', { method: 'github' }) after saving (or immediately before
returning) so the verification funnel is recorded; update the block that assigns
developer.isVerified = true and saves in the function handling the GitHub
profile to conditionally emit this event when transitioning from unverified to
verified.
apps/dashboard-api/src/controllers/analytics.controller.js (3)

115-115: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Fix response format to match API standard.

The response does not follow the required { success: bool, data: {}, message: "" } format.

🔧 Proposed fix
-   res.json(formattedLogs);
+   res.json({ success: true, data: formattedLogs, message: '' });

As per coding guidelines: "All API endpoints return: { success: bool, data: {}, message: "" }."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/dashboard-api/src/controllers/analytics.controller.js` at line 115, The
controller currently returns raw data via res.json(formattedLogs); update the
handler (the function that sends formattedLogs in
apps/dashboard-api/src/controllers/analytics.controller.js) to wrap the payload
in the standard envelope by returning res.json({ success: true, data:
formattedLogs, message: "" }) for successful responses (and similarly use {
success:false, data:{}, message: "..." } for error paths) so all endpoints
conform to the `{ success: bool, data: {}, message: "" }` API contract.

86-88: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Replace raw error exposure with AppError.

The catch block directly exposes err.message to the client, which could leak internal MongoDB error details. As per coding guidelines, use the AppError class for errors and never expose MongoDB errors to the client.

🛡️ Proposed fix
  } catch (err) {
-   res.status(500).json({ success: false, data: {}, message: err.message });
+   console.error('getGlobalStats error:', err);
+   throw new AppError('Failed to retrieve global statistics', 500);
  }

As per coding guidelines: "Use AppError class for errors — never raw throw, never expose MongoDB errors to client."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/dashboard-api/src/controllers/analytics.controller.js` around lines 86 -
88, The catch currently returns err.message to the client; instead log the
original err internally (e.g., console.error(err)) and replace the response with
an AppError instance: create and pass new AppError('Internal server error', 500)
to the Express error handler via next(new AppError(...)) (ensure the controller
signature includes next), removing any use of err.message in
res.status(...).json and keeping only a generic message to the client.

116-118: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Replace raw error exposure with AppError and fix response format.

The catch block uses the wrong response format and directly exposes err.message, which could leak internal MongoDB error details.

🛡️ Proposed fix
  } catch (err) {
-   res.status(500).json({ error: err.message });
+   console.error('getRecentActivity error:', err);
+   throw new AppError('Failed to retrieve recent activity', 500);
  }

As per coding guidelines: "All API endpoints return: { success: bool, data: {}, message: "" }. Use AppError class for errors — never raw throw, never expose MongoDB errors to client."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/dashboard-api/src/controllers/analytics.controller.js` around lines 116
- 118, In the catch block of the analytics controller replace the direct
response that exposes err.message with error-handling that uses the AppError
class and the route's next() so the centralized error middleware formats the
response as { success:false, data:{}, message:"" }; specifically, remove
res.status(500).json({ error: err.message }) and instead log the original err
(e.g., using console.error or processLogger.error) and call next(new
AppError(500, "Internal Server Error")) so no MongoDB/internal messages are sent
to the client and the global error handler returns the standardized payload.
🧹 Nitpick comments (10)
packages/common/src/models/DeveloperActivity.js (2)

27-30: ⚡ Quick win

Add validation to prevent negative activity counters.

The activity counters (apiCallCount, mailSentCount, etc.) default to 0 but have no minimum constraint. If rollup logic uses $inc operations without validation, bugs could introduce negative values. Consider adding min: 0 validators or ensure rollup code validates increments.

🛡️ Add minimum validators
-   apiCallCount: { type: Number, default: 0 },
-   mailSentCount: { type: Number, default: 0 },
-   storageUploadsCount: { type: Number, default: 0 },
-   webhookTriggeredCount: { type: Number, default: 0 },
+   apiCallCount: { type: Number, default: 0, min: 0 },
+   mailSentCount: { type: Number, default: 0, min: 0 },
+   storageUploadsCount: { type: Number, default: 0, min: 0 },
+   webhookTriggeredCount: { type: Number, default: 0, min: 0 },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/common/src/models/DeveloperActivity.js` around lines 27 - 30, The
numeric activity fields in DeveloperActivity (apiCallCount, mailSentCount,
storageUploadsCount, webhookTriggeredCount) need non-negative validation; update
the Mongoose schema for those fields to include a min: 0 validator (or
equivalent validation) so attempts to set negative values are rejected, and keep
the existing default: 0; ensure any rollup/`$inc` paths that update these fields
still rely on schema validation or add runtime checks to prevent negative
results when applying decrements.

23-26: ⚖️ Poor tradeoff

Consider size limits for activeProjectIds array.

The activeProjectIds array is unbounded and could grow large for highly active developers with many projects. While unlikely to hit MongoDB's 16MB document limit in practice, consider documenting expected max size or adding application-level limits if a developer can create hundreds of projects.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/common/src/models/DeveloperActivity.js` around lines 23 - 26, The
activeProjectIds array in the DeveloperActivity mongoose schema is unbounded and
could grow large; add an application-level limit and validation to prevent
excessive growth by updating the activeProjectIds path in DeveloperActivity.js
to enforce a maximum array length (e.g., via Mongoose's validate or maxlength
option) and document the expected max entries in the model comment/README;
alternatively, if many project refs are expected, move these IDs to a separate
collection or paginated subdocument store and update any functions that push/pop
project IDs to respect the new limit and surface a clear error when exceeded.
packages/common/src/models/PlatformEvent.js (3)

38-41: ⚖️ Poor tradeoff

Consider size limits and validation for the properties field.

The properties field uses Mixed type with no size constraints. Unbounded Mixed fields can lead to document bloat, slow queries, and eventual MongoDB document size limit (16MB) issues. Consider adding application-level validation or documentation specifying max size and allowed keys.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/common/src/models/PlatformEvent.js` around lines 38 - 41, The
PlatformEvent model's properties field is currently mongoose.Schema.Types.Mixed
with no constraints; add validation to prevent oversized or unexpected keys by:
implement a custom validator on the properties field in the PlatformEvent schema
(or replace Mixed with a stricter subdocument/schema) that enforces a max
serialized size (e.g., JSON.stringify(properties).length <= X bytes) and
optionally restricts allowed top-level keys (whitelist) or depth, and update any
create/update paths that set properties to ensure they respect this validation
and return clear errors; reference the properties field in the PlatformEvent
schema and the model construction to locate where to add the validator or nested
schema.

24-24: ⚡ Quick win

Remove redundant individual index on developerId.

The individual index { developerId: 1 } at line 24 is redundant because the compound index { developerId: 1, event: 1, timestamp: -1 } at line 51 can serve queries on developerId alone via index prefix scanning. Keeping both wastes storage and slows down writes.

♻️ Remove the redundant index
    developerId: {
      type: mongoose.Schema.Types.ObjectId,
      ref: 'Developer',
      required: true,
-     index: true,
    },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/common/src/models/PlatformEvent.js` at line 24, Remove the redundant
single-field index on developerId: the model currently defines an individual
index { developerId: 1 } and also a compound index { developerId: 1, event: 1,
timestamp: -1 } (in PlatformEvent.js); drop the single-field index declaration
for developerId so queries can use the compound index prefix and avoid extra
storage and write overhead.

35-35: ⚡ Quick win

Remove redundant individual index on event.

The individual index { event: 1 } at line 35 is redundant because the compound index { event: 1, timestamp: -1 } at line 52 already provides efficient lookups on event alone via index prefix. Duplicate indexes increase write overhead and storage costs.

♻️ Remove the redundant index
    event: {
      type: String,
      required: true,
-     index: true,
    },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/common/src/models/PlatformEvent.js` at line 35, Remove the redundant
single-field index on event in the PlatformEvent model: locate where the
schema/indexes are defined (the entries `{ event: 1 }` and the compound `{
event: 1, timestamp: -1 }`) and delete the individual `{ event: 1 }` index so
only the compound `{ event: 1, timestamp: -1 }` remains; this reduces duplicate
index overhead while preserving query performance via the compound index prefix.
apps/public-api/src/middlewares/api_usage.js (1)

83-84: 💤 Low value

Hoist the @urbackend/common require to the top of the file.

Calling require('@urbackend/common') inside the setImmediate callback works because Node caches the resolved module, but it's the only inline require in this file and it makes the dependency graph harder to read at a glance. Both Project and PlatformEvent are already exported by the package that's imported at the top of this file.

♻️ Suggested change
-const { Log, redis, ApiAnalytics } = require('@urbackend/common');
+const { Log, redis, ApiAnalytics, Project, PlatformEvent } = require('@urbackend/common');
...
-                            const { Project, PlatformEvent } = require('@urbackend/common');
-                            const proj = await Project.findById(req.project._id).select('owner').lean();
+                            const proj = await Project.findById(req.project._id).select('owner').lean();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/public-api/src/middlewares/api_usage.js` around lines 83 - 84, Remove
the inline require inside the setImmediate callback and hoist the module import
to the top of the file by adding a top-level const { Project, PlatformEvent } =
require('@urbackend/common');; then update the setImmediate callback to use the
already-imported Project and PlatformEvent (the occurrences around the
setImmediate where proj is fetched with Project.findById and any PlatformEvent
usage). Ensure there are no other inline requires for '@urbackend/common' left
in this file.
apps/dashboard-api/src/routes/admin.metrics.js (1)

14-22: ⚡ Quick win

Defense-in-depth: enforce isAdmin at the router level too.

Right now every route is authMiddleware-only and relies on each controller calling requireAdmin() internally. That's correct today, but the contract is invisible from the router and a new admin endpoint added later (or a refactor that drops the in-controller guard) will silently expose admin-only data to any logged-in developer. Adding the admin check once at the router is cheap and removes the coupling.

♻️ Suggested change
 const authMiddleware = require('../middlewares/authMiddleware');
+const requireAdmin = (req, res, next) =>
+  req.user?.isAdmin ? next() : res.status(403).json({ success: false, data: {}, message: 'Admin access required.' });
+
+router.use(authMiddleware, requireAdmin);
 ...
-router.get('/overview', authMiddleware, getOverview);
-router.get('/activation-funnel', authMiddleware, getActivationFunnel);
-router.get('/cohorts', authMiddleware, getCohorts);
-router.get('/feature-usage', authMiddleware, getFeatureUsage);
-router.get('/reliability', authMiddleware, getReliability);
-router.get('/top-projects', authMiddleware, getTopProjects);
-router.get('/churn-signals', authMiddleware, getChurnSignals);
+router.get('/overview', getOverview);
+router.get('/activation-funnel', getActivationFunnel);
+router.get('/cohorts', getCohorts);
+router.get('/feature-usage', getFeatureUsage);
+router.get('/reliability', getReliability);
+router.get('/top-projects', getTopProjects);
+router.get('/churn-signals', getChurnSignals);

(Adjust the requireAdmin body to match the existing project convention — AppError + the { success, data, message } envelope from your controller guidelines.)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/dashboard-api/src/routes/admin.metrics.js` around lines 14 - 22, Add an
explicit admin-check middleware to the router so admin-only routes use both
authMiddleware and requireAdmin at the route level (e.g., change
router.get('/overview', authMiddleware, getOverview) to router.get('/overview',
authMiddleware, requireAdmin, getOverview) for all listed routes like
'/overview', '/activation-funnel', '/cohorts', etc. Also update the requireAdmin
implementation to follow project conventions by throwing an AppError (or passing
an AppError to next) and returning the controller response envelope { success,
data, message } on denial so the router-level guard matches existing
error/response handling.
apps/public-api/src/utils/emitEvent.js (1)

1-26: ⚡ Quick win

Duplicated emitEvent helper across services — consolidate into @urbackend/common.

This file is functionally identical to apps/dashboard-api/src/utils/emitEvent.js (same signature, same setImmediate + PlatformEvent.create body, same logging). Keeping two copies means future changes (tracing, sampling, batching, retry) will have to be applied in both places and will inevitably drift. Since PlatformEvent is already exported from @urbackend/common, the helper belongs there too.

Also note Project is imported but never used here.

♻️ Suggested direction

Move the helper to packages/common/src/utils/emitEvent.js, re-export from packages/common/src/index.js, and replace both app-level copies with:

-const { PlatformEvent, Project } = require('@urbackend/common');
-
-function emitEvent(developerId, event, properties = {}, projectId = null) {
-  setImmediate(async () => {
-    try {
-      await PlatformEvent.create({ ... });
-    } catch (err) {
-      console.error(`[emitEvent] Failed to write "${event}":`, err.message);
-    }
-  });
-}
-
-module.exports = { emitEvent };
+const { emitEvent } = require('@urbackend/common');
+module.exports = { emitEvent };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/public-api/src/utils/emitEvent.js` around lines 1 - 26, The emitEvent
helper is duplicated across services; move the function (signature
emitEvent(developerId, event, properties = {}, projectId = null) that uses
setImmediate and PlatformEvent.create) into the shared `@urbackend/common` utils,
re-export it from the common package's public index, then update both app-level
copies to import { emitEvent } from '@urbackend/common' instead of declaring it
locally; also remove the unused Project import from the original file and ensure
behavior (fire-and-forget, never throwing, same log message) remains identical
after relocation.
apps/web-dashboard/src/pages/AdminMetrics.jsx (1)

105-106: 💤 Low value

Remove unnecessary queueMicrotask wrapper from these useEffect hooks.

The pattern of deferring callback execution with queueMicrotask is unusual and lacks a documented reason. Both callbacks can be called directly without the microtask deferral—the effect already runs at the appropriate time after render. This adds unnecessary complexity and makes debugging harder:

Suggested change
-  useEffect(() => { queueMicrotask(() => load()); }, [load]);
-  useEffect(() => { queueMicrotask(() => loadCohort()); }, [loadCohort]);
+  useEffect(() => { load(); }, [load]);
+  useEffect(() => { loadCohort(); }, [loadCohort]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web-dashboard/src/pages/AdminMetrics.jsx` around lines 105 - 106, Remove
the unnecessary queueMicrotask wrappers inside the two useEffect hooks: call
load and loadCohort directly from their respective useEffect callbacks instead
of wrapping them in queueMicrotask; update the effect bodies that reference load
and loadCohort so they simply invoke load() and loadCohort() (preserving the
dependency arrays) to simplify execution and make behavior easier to debug.
apps/dashboard-api/src/controllers/analytics.controller.js (1)

244-244: ⚡ Quick win

Consider optimizing unique project ID collection in aggregation.

The current approach uses $push: '$activeProjectIds' which creates nested arrays (since activeProjectIds is already an array), then flattens them on line 258. For developers with many active days or projects, this could be memory-intensive.

♻️ Proposed optimization

Add an $unwind stage before $group to flatten activeProjectIds upfront:

    const agg = await DeveloperActivity.aggregate([
      {
        $match: {
          developerId: new mongoose.Types.ObjectId(developerId),
          date: { $gte: thirtyDaysAgo },
        },
      },
+     { $unwind: { path: '$activeProjectIds', preserveNullAndEmptyArrays: true } },
      {
        $group: {
          _id: null,
          totalApiCalls: { $sum: '$apiCallCount' },
          totalMailSent: { $sum: '$mailSentCount' },
          totalStorageUploads: { $sum: '$storageUploadsCount' },
          totalWebhooksFired: { $sum: '$webhookTriggeredCount' },
          activeDays: { $sum: 1 },
-         allProjectIds: { $push: '$activeProjectIds' },
+         allProjectIds: { $addToSet: '$activeProjectIds' },
        },
      },
    ]);

Then simplify line 259:

-   const flatProjectIds = (result.allProjectIds || []).flat();
-   const uniqueActiveProjects = new Set(flatProjectIds.map(String)).size;
+   const uniqueActiveProjects = (result.allProjectIds || []).filter(Boolean).length;

Note: The activeDays count would need adjustment if you want distinct days (currently it counts per unwound document).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/dashboard-api/src/controllers/analytics.controller.js` at line 244, The
aggregation currently uses a $push of the array field activeProjectIds which
creates nested arrays and is memory-inefficient; update the pipeline used in the
analytics aggregation (the variable/array building the Mongo pipeline in the
analytics controller) to $unwind the activeProjectIds field before the $group
stage so each project id is emitted as a single value, then replace the group
accumulator allProjectIds: { $push: '$activeProjectIds' } with allProjectIds: {
$addToSet: '$activeProjectIds' } to collect unique project IDs without nested
arrays; if you require distinct activeDays instead of counting unwound
documents, adjust the activeDays calculation accordingly (e.g., use a separate
$addToSet on the day field before counting).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/dashboard-api/src/controllers/admin.metrics.controller.js`:
- Around line 55-57: The catch blocks in admin.metrics.controller.js currently
return raw err.message to clients (e.g., the catch that sends
res.status(500).json({ success: false, data: {}, message: err.message }));
change each catch to log the full error internally (use the existing logger or
console.error) and return a generic client-facing message like "Internal server
error" or "An unexpected error occurred" while preserving success:false and
data:{}; apply this pattern to every catch in this file (the ones referenced in
the review) so controllers such as the admin metrics handlers never expose
MongoDB/internal error details to clients.
- Around line 147-166: checkDayRetention currently issues a
DeveloperActivity.findOne per signup causing N+1 queries; instead, for a given
daysAfter compute the per-cohort target and next UTC date boundaries, collect
all developerIds from signups, run a single DeveloperActivity.find (or
aggregate) with { developerId: { $in: developerIds }, date: { $gte: target, $lt:
next } } and count distinct developerId results to produce retained; update
checkDayRetention to accept signups and daysAfter and return the deduplicated
count (use DeveloperActivity.distinct or an aggregation pipeline) so you perform
one DB query per retention day rather than one per signup.

In `@apps/dashboard-api/src/controllers/analytics.controller.js`:
- Around line 318-320: Replace the direct exposure of err.message in the catch
block inside the analytics controller (the catch after the function that sends
analytics response) by wrapping the original error in the AppError class and
delegating to the global error handler instead of sending raw error text;
specifically, construct a new AppError with a safe client-facing message (e.g.,
"Failed to fetch analytics") and an appropriate status code, attach the original
error as metadata if needed, and call next(new AppError(...)) (or pass it into
the centralized error handler) rather than doing res.status(500).json({...,
message: err.message}).
- Around line 274-276: In the analytics controller catch block that currently
does res.status(500).json({ success: false, data: {}, message: err.message }),
stop exposing err.message; import/use the AppError class and replace that
response with forwarding a sanitized AppError to Express (e.g. next(new
AppError('Internal server error', 500))) and log the original err to server logs
(console.error or the existing logger) so internal MongoDB details aren't
returned to clients.
- Around line 159-161: The catch block in analytics.controller is exposing raw
err.message to clients; instead import and use the AppError class and the
Express error flow: replace res.status(500).json({...err.message}) with logging
the original error (e.g., logger.error(err) or console.error(err)) and call
next(new AppError('Internal server error', 500)); ensure the controller function
signature accepts next and add the AppError import (AppError) so no
MongoDB/internal messages are returned to clients.
- Around line 213-215: The catch block in the analytics controller currently
sends err.message to the client (res.status(500).json(...)), exposing internal
errors; replace this with creation/forwarding of an AppError so clients only
receive a generic message. Inside the catch, log the original err for server
diagnostics, then call next(new AppError('Internal server error', 500)) (or
construct an AppError and pass to next) instead of using err.message; reference
the catch's err variable, res usage and the AppError class to implement this
change in the analytics controller method.

In `@apps/dashboard-api/src/controllers/events.controller.js`:
- Around line 25-27: The 400 response in the events controller returns {
success, message } but must include a data field per guidelines; update the
handler that checks the event variable (the block validating "event" and calling
res.status(400).json(...)) to return { success: false, data: {}, message: 'event
name is required' } instead of the current shape so all responses include the
required data key.
- Around line 31-36: The JSON error response returned when
ALLOWED_FRONTEND_EVENTS does not contain normalizedEvent must follow the `{
success: bool, data: {}, message: "" }` order; update the response in the
controller (the block that checks `ALLOWED_FRONTEND_EVENTS.has(normalizedEvent)`
and calls `res.status(400).json(...)`) to return `success: false`, `data: {}`
and `message: "Unknown event: \"...\". Allowed: ..."` in that exact order while
keeping the 400 status and the same message content.
- Around line 47-50: The catch block in the events controller currently logs the
raw error and sends a raw 500 response; replace this with the AppError pattern:
import AppError from '@urbackend/common' and in the catch handler do not expose
err to the client—log it internally if needed but call next(new
AppError('Internal server error', 500)) (or throw new AppError(...)) instead of
res.status(500).json(...); ensure the unique catch block in events.controller
(the handler that currently does console.error('[events.controller] track
error:', err)) uses AppError and does not include err details in the
client-facing message.

In `@apps/public-api/src/middlewares/api_usage.js`:
- Around line 77-103: The NX Redis set for the activation flag (flagKey / the
redis.set call inside the setImmediate block) currently writes a permanent key;
change it to include a 2-year TTL so the key is set NX with expiry (e.g., use
redis.set(flagKey, '1', 'NX', 'EX', 63072000) or equivalent setnx + expire
sequence if your Redis client requires different args) to prevent re-emission
after a Redis flush or eviction.

In `@apps/web-dashboard/src/components/Dashboard/DeveloperMetrics.jsx`:
- Around line 21-31: The component currently swallows fetch errors and returns
null; add an error state (e.g., error, setError) in the DeveloperMetrics
component, set setError(err) in the catch of fetchMetrics (and clear error
before retries), and update the render logic so that when error is truthy you
render a user-facing error message and a retry control that calls fetchMetrics
again; keep using the existing loading, metrics, setLoading semantics so loading
still shows while fetching and metrics renders when successful.

In `@apps/web-dashboard/src/pages/AdminMetrics.jsx`:
- Around line 88-90: The catch block in AdminMetrics.jsx uses brittle string
matching on e.message to decide to call navigate('/dashboard') or setError;
change it to check a structured error property (e.g., e.code ===
'ADMIN_REQUIRED') first and fall back to the existing message check, so replace
the current string-only test with a check for a standardized error code (and
only then fallback to message includes), updating the handling around navigate
and setError accordingly; coordinate with the backend to return an error object
with a code (e.g., 'ADMIN_REQUIRED') so the front-end can reliably branch on
e.code while preserving the current fallback behavior.

In `@packages/common/src/models/PlatformEvent.js`:
- Around line 55-58: The TTL index on platformEventSchema is correct but we
should add a clarifying comment and safeguard to prevent accidental expiry
surprises: update the PlatformEvent.js near platformEventSchema.index to
document that expireAfterSeconds is calculated from the document's timestamp
field value (which defaults to Date.now) and warn that any explicit timestamp
overrides (e.g., in emitEvent.js and reliabilityAlertQueue.js where timestamp:
new Date() is set) will affect expiry; also ensure the schema keeps default:
Date.now and consider adding a unit test or runtime assertion in
emitEvent.js/reliabilityAlertQueue.js that timestamp is not set to a past date
before insert.

In `@packages/common/src/queues/activityRollupQueue.js`:
- Around line 64-78: The aggregation built by Log.aggregate currently computes
apiCallCount, mailCount, and storageCount but omits webhooks; add a
webhookTriggeredCount field to the pipeline (similar to mailCount/storageCount)
using a $sum with a $cond and $regexMatch on the request path (e.g., match
"/api/webhook" or "/api/webhooks" as your routes use) so webhook calls are
counted between dayStart and dayEnd, then update the downstream mapping logic
that reads logAgg results into the developer map to use this new
webhookTriggeredCount value instead of leaving webhookTriggeredCount initialized
to 0.

---

Outside diff comments:
In `@apps/dashboard-api/src/controllers/analytics.controller.js`:
- Line 115: The controller currently returns raw data via
res.json(formattedLogs); update the handler (the function that sends
formattedLogs in apps/dashboard-api/src/controllers/analytics.controller.js) to
wrap the payload in the standard envelope by returning res.json({ success: true,
data: formattedLogs, message: "" }) for successful responses (and similarly use
{ success:false, data:{}, message: "..." } for error paths) so all endpoints
conform to the `{ success: bool, data: {}, message: "" }` API contract.
- Around line 86-88: The catch currently returns err.message to the client;
instead log the original err internally (e.g., console.error(err)) and replace
the response with an AppError instance: create and pass new AppError('Internal
server error', 500) to the Express error handler via next(new AppError(...))
(ensure the controller signature includes next), removing any use of err.message
in res.status(...).json and keeping only a generic message to the client.
- Around line 116-118: In the catch block of the analytics controller replace
the direct response that exposes err.message with error-handling that uses the
AppError class and the route's next() so the centralized error middleware
formats the response as { success:false, data:{}, message:"" }; specifically,
remove res.status(500).json({ error: err.message }) and instead log the original
err (e.g., using console.error or processLogger.error) and call next(new
AppError(500, "Internal Server Error")) so no MongoDB/internal messages are sent
to the client and the global error handler returns the standardized payload.

In `@apps/dashboard-api/src/controllers/auth.controller.js`:
- Around line 161-189: When reconciling an existing Developer in the GitHub flow
(the branch that finds Developer via Developer.findOne and sets
developer.githubId/githubUsername/avatarUrl/isVerified), check the previous
isVerified value and, if it was false, call emitEvent(developer._id,
'email_verified', { method: 'github' }) after saving (or immediately before
returning) so the verification funnel is recorded; update the block that assigns
developer.isVerified = true and saves in the function handling the GitHub
profile to conditionally emit this event when transitioning from unverified to
verified.

---

Nitpick comments:
In `@apps/dashboard-api/src/controllers/analytics.controller.js`:
- Line 244: The aggregation currently uses a $push of the array field
activeProjectIds which creates nested arrays and is memory-inefficient; update
the pipeline used in the analytics aggregation (the variable/array building the
Mongo pipeline in the analytics controller) to $unwind the activeProjectIds
field before the $group stage so each project id is emitted as a single value,
then replace the group accumulator allProjectIds: { $push: '$activeProjectIds' }
with allProjectIds: { $addToSet: '$activeProjectIds' } to collect unique project
IDs without nested arrays; if you require distinct activeDays instead of
counting unwound documents, adjust the activeDays calculation accordingly (e.g.,
use a separate $addToSet on the day field before counting).

In `@apps/dashboard-api/src/routes/admin.metrics.js`:
- Around line 14-22: Add an explicit admin-check middleware to the router so
admin-only routes use both authMiddleware and requireAdmin at the route level
(e.g., change router.get('/overview', authMiddleware, getOverview) to
router.get('/overview', authMiddleware, requireAdmin, getOverview) for all
listed routes like '/overview', '/activation-funnel', '/cohorts', etc. Also
update the requireAdmin implementation to follow project conventions by throwing
an AppError (or passing an AppError to next) and returning the controller
response envelope { success, data, message } on denial so the router-level guard
matches existing error/response handling.

In `@apps/public-api/src/middlewares/api_usage.js`:
- Around line 83-84: Remove the inline require inside the setImmediate callback
and hoist the module import to the top of the file by adding a top-level const {
Project, PlatformEvent } = require('@urbackend/common');; then update the
setImmediate callback to use the already-imported Project and PlatformEvent (the
occurrences around the setImmediate where proj is fetched with Project.findById
and any PlatformEvent usage). Ensure there are no other inline requires for
'@urbackend/common' left in this file.

In `@apps/public-api/src/utils/emitEvent.js`:
- Around line 1-26: The emitEvent helper is duplicated across services; move the
function (signature emitEvent(developerId, event, properties = {}, projectId =
null) that uses setImmediate and PlatformEvent.create) into the shared
`@urbackend/common` utils, re-export it from the common package's public index,
then update both app-level copies to import { emitEvent } from
'@urbackend/common' instead of declaring it locally; also remove the unused
Project import from the original file and ensure behavior (fire-and-forget,
never throwing, same log message) remains identical after relocation.

In `@apps/web-dashboard/src/pages/AdminMetrics.jsx`:
- Around line 105-106: Remove the unnecessary queueMicrotask wrappers inside the
two useEffect hooks: call load and loadCohort directly from their respective
useEffect callbacks instead of wrapping them in queueMicrotask; update the
effect bodies that reference load and loadCohort so they simply invoke load()
and loadCohort() (preserving the dependency arrays) to simplify execution and
make behavior easier to debug.

In `@packages/common/src/models/DeveloperActivity.js`:
- Around line 27-30: The numeric activity fields in DeveloperActivity
(apiCallCount, mailSentCount, storageUploadsCount, webhookTriggeredCount) need
non-negative validation; update the Mongoose schema for those fields to include
a min: 0 validator (or equivalent validation) so attempts to set negative values
are rejected, and keep the existing default: 0; ensure any rollup/`$inc` paths
that update these fields still rely on schema validation or add runtime checks
to prevent negative results when applying decrements.
- Around line 23-26: The activeProjectIds array in the DeveloperActivity
mongoose schema is unbounded and could grow large; add an application-level
limit and validation to prevent excessive growth by updating the
activeProjectIds path in DeveloperActivity.js to enforce a maximum array length
(e.g., via Mongoose's validate or maxlength option) and document the expected
max entries in the model comment/README; alternatively, if many project refs are
expected, move these IDs to a separate collection or paginated subdocument store
and update any functions that push/pop project IDs to respect the new limit and
surface a clear error when exceeded.

In `@packages/common/src/models/PlatformEvent.js`:
- Around line 38-41: The PlatformEvent model's properties field is currently
mongoose.Schema.Types.Mixed with no constraints; add validation to prevent
oversized or unexpected keys by: implement a custom validator on the properties
field in the PlatformEvent schema (or replace Mixed with a stricter
subdocument/schema) that enforces a max serialized size (e.g.,
JSON.stringify(properties).length <= X bytes) and optionally restricts allowed
top-level keys (whitelist) or depth, and update any create/update paths that set
properties to ensure they respect this validation and return clear errors;
reference the properties field in the PlatformEvent schema and the model
construction to locate where to add the validator or nested schema.
- Line 24: Remove the redundant single-field index on developerId: the model
currently defines an individual index { developerId: 1 } and also a compound
index { developerId: 1, event: 1, timestamp: -1 } (in PlatformEvent.js); drop
the single-field index declaration for developerId so queries can use the
compound index prefix and avoid extra storage and write overhead.
- Line 35: Remove the redundant single-field index on event in the PlatformEvent
model: locate where the schema/indexes are defined (the entries `{ event: 1 }`
and the compound `{ event: 1, timestamp: -1 }`) and delete the individual `{
event: 1 }` index so only the compound `{ event: 1, timestamp: -1 }` remains;
this reduces duplicate index overhead while preserving query performance via the
compound index prefix.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 096de59e-93a2-49bb-98f2-755eeea33f30

📥 Commits

Reviewing files that changed from the base of the PR and between 85c53f1 and 543c202.

📒 Files selected for processing (25)
  • apps/dashboard-api/src/__tests__/auth.controller.test.js
  • apps/dashboard-api/src/app.js
  • apps/dashboard-api/src/controllers/admin.metrics.controller.js
  • apps/dashboard-api/src/controllers/analytics.controller.js
  • apps/dashboard-api/src/controllers/auth.controller.js
  • apps/dashboard-api/src/controllers/events.controller.js
  • apps/dashboard-api/src/controllers/project.controller.js
  • apps/dashboard-api/src/routes/admin.metrics.js
  • apps/dashboard-api/src/routes/analytics.js
  • apps/dashboard-api/src/routes/events.js
  • apps/dashboard-api/src/utils/emitEvent.js
  • apps/public-api/src/app.js
  • apps/public-api/src/middlewares/api_usage.js
  • apps/public-api/src/utils/emitEvent.js
  • apps/web-dashboard/src/App.jsx
  • apps/web-dashboard/src/components/Dashboard/DeveloperMetrics.jsx
  • apps/web-dashboard/src/index.css
  • apps/web-dashboard/src/pages/AdminMetrics.jsx
  • apps/web-dashboard/src/pages/Dashboard.jsx
  • packages/common/src/index.js
  • packages/common/src/models/DeveloperActivity.js
  • packages/common/src/models/PlatformEvent.js
  • packages/common/src/models/index.js
  • packages/common/src/queues/activityRollupQueue.js
  • packages/common/src/queues/reliabilityAlertQueue.js

Comment thread apps/dashboard-api/src/controllers/admin.metrics.controller.js
Comment thread apps/dashboard-api/src/controllers/admin.metrics.controller.js Outdated
Comment on lines +159 to +161
} catch (err) {
res.status(500).json({ success: false, data: {}, message: err.message });
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Replace raw error exposure with AppError.

The catch block directly exposes err.message to the client, which could leak internal MongoDB error details.

🛡️ Proposed fix
  } catch (err) {
-   res.status(500).json({ success: false, data: {}, message: err.message });
+   console.error('getActivationFunnel error:', err);
+   throw new AppError('Failed to retrieve activation funnel', 500);
  }

As per coding guidelines: "Use AppError class for errors — never raw throw, never expose MongoDB errors to client."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/dashboard-api/src/controllers/analytics.controller.js` around lines 159
- 161, The catch block in analytics.controller is exposing raw err.message to
clients; instead import and use the AppError class and the Express error flow:
replace res.status(500).json({...err.message}) with logging the original error
(e.g., logger.error(err) or console.error(err)) and call next(new
AppError('Internal server error', 500)); ensure the controller function
signature accepts next and add the AppError import (AppError) so no
MongoDB/internal messages are returned to clients.

Comment on lines +213 to +215
} catch (err) {
res.status(500).json({ success: false, data: {}, message: err.message });
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Replace raw error exposure with AppError.

The catch block directly exposes err.message to the client, which could leak internal MongoDB error details.

🛡️ Proposed fix
  } catch (err) {
-   res.status(500).json({ success: false, data: {}, message: err.message });
+   console.error('getRetention error:', err);
+   throw new AppError('Failed to retrieve retention data', 500);
  }

As per coding guidelines: "Use AppError class for errors — never raw throw, never expose MongoDB errors to client."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (err) {
res.status(500).json({ success: false, data: {}, message: err.message });
}
} catch (err) {
console.error('getRetention error:', err);
throw new AppError('Failed to retrieve retention data', 500);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/dashboard-api/src/controllers/analytics.controller.js` around lines 213
- 215, The catch block in the analytics controller currently sends err.message
to the client (res.status(500).json(...)), exposing internal errors; replace
this with creation/forwarding of an AppError so clients only receive a generic
message. Inside the catch, log the original err for server diagnostics, then
call next(new AppError('Internal server error', 500)) (or construct an AppError
and pass to next) instead of using err.message; reference the catch's err
variable, res usage and the AppError class to implement this change in the
analytics controller method.

Comment on lines +274 to +276
} catch (err) {
res.status(500).json({ success: false, data: {}, message: err.message });
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Replace raw error exposure with AppError.

The catch block directly exposes err.message to the client, which could leak internal MongoDB error details.

🛡️ Proposed fix
  } catch (err) {
-   res.status(500).json({ success: false, data: {}, message: err.message });
+   console.error('getEngagement error:', err);
+   throw new AppError('Failed to retrieve engagement data', 500);
  }

As per coding guidelines: "Use AppError class for errors — never raw throw, never expose MongoDB errors to client."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/dashboard-api/src/controllers/analytics.controller.js` around lines 274
- 276, In the analytics controller catch block that currently does
res.status(500).json({ success: false, data: {}, message: err.message }), stop
exposing err.message; import/use the AppError class and replace that response
with forwarding a sanitized AppError to Express (e.g. next(new
AppError('Internal server error', 500))) and log the original err to server logs
(console.error or the existing logger) so internal MongoDB details aren't
returned to clients.

Comment thread apps/public-api/src/middlewares/api_usage.js
Comment on lines +21 to +31
} catch (err) {
console.error('Failed to load personal metrics', err);
} finally {
setLoading(false);
}
};

fetchMetrics();
}, []);

if (loading || !metrics) return null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Consider showing error state to users.

The component logs fetch errors to the console but renders nothing on failure. Users won't know if metrics failed to load versus still loading. Consider adding an error state and displaying a message or retry option.

💡 Suggested improvement
 export default function DeveloperMetrics() {
   const [metrics, setMetrics] = useState(null);
   const [loading, setLoading] = useState(true);
+  const [error, setError] = useState(null);

   useEffect(() => {
     const fetchMetrics = async () => {
       try {
         const [funnelRes, engRes] = await Promise.all([
           api.get('/api/analytics/funnel'),
           api.get('/api/analytics/engagement')
         ]);
         
         setMetrics({
           funnel: funnelRes.data?.data,
           engagement: engRes.data?.data
         });
       } catch (err) {
         console.error('Failed to load personal metrics', err);
+        setError('Failed to load metrics');
       } finally {
         setLoading(false);
       }
     };

     fetchMetrics();
   }, []);

-  if (loading || !metrics) return null;
+  if (loading) return null;
+  if (error) return <div style={{color: 'var(--color-error)'}}>{error}</div>;
+  if (!metrics) return null;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web-dashboard/src/components/Dashboard/DeveloperMetrics.jsx` around
lines 21 - 31, The component currently swallows fetch errors and returns null;
add an error state (e.g., error, setError) in the DeveloperMetrics component,
set setError(err) in the catch of fetchMetrics (and clear error before retries),
and update the render logic so that when error is truthy you render a
user-facing error message and a retry control that calls fetchMetrics again;
keep using the existing loading, metrics, setLoading semantics so loading still
shows while fetching and metrics renders when successful.

Comment on lines +88 to +90
} catch (e) {
if (e.message?.includes('Admin')) navigate('/dashboard');
else setError(e.message);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

String-based error detection is fragile.

Checking if e.message?.includes('Admin') to determine redirect behavior is brittle. If the backend error message changes or is localized, this logic will break. Consider using error codes or a structured error response.

💡 Suggested improvement

Coordinate with the backend to return structured errors:

// Backend: throw new AppError('Admin access required', 403, 'ADMIN_REQUIRED');

// Frontend:
} catch (e) {
  if (e.code === 'ADMIN_REQUIRED' || e.message?.includes('Admin')) {
    navigate('/dashboard');
  } else {
    setError(e.message);
  }
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web-dashboard/src/pages/AdminMetrics.jsx` around lines 88 - 90, The
catch block in AdminMetrics.jsx uses brittle string matching on e.message to
decide to call navigate('/dashboard') or setError; change it to check a
structured error property (e.g., e.code === 'ADMIN_REQUIRED') first and fall
back to the existing message check, so replace the current string-only test with
a check for a standardized error code (and only then fallback to message
includes), updating the handling around navigate and setError accordingly;
coordinate with the backend to return an error object with a code (e.g.,
'ADMIN_REQUIRED') so the front-end can reliably branch on e.code while
preserving the current fallback behavior.

Comment on lines +55 to +58
platformEventSchema.index(
{ timestamp: 1 },
{ expireAfterSeconds: 730 * 24 * 60 * 60 },
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Search for PlatformEvent.create calls with timestamp field

rg -n -A5 -B2 --type=js --type=ts 'PlatformEvent\.create' | rg -C3 'timestamp'

Repository: geturbackend/urBackend

Length of output: 876


🏁 Script executed:

# Find the PlatformEvent model definition
find . -name "PlatformEvent*" -type f | head -20

Repository: geturbackend/urBackend

Length of output: 112


🏁 Script executed:

# Search for the model file location and examine timestamp field definition
rg -l "platformEventSchema" --type=js --type=ts

Repository: geturbackend/urBackend

Length of output: 110


🏁 Script executed:

# Check for all PlatformEvent instantiation patterns beyond .create()
rg -n "new PlatformEvent|PlatformEvent\.save|PlatformEvent\.insert" --type=js --type=ts | head -20

Repository: geturbackend/urBackend

Length of output: 174


🏁 Script executed:

cat packages/common/src/models/PlatformEvent.js

Repository: geturbackend/urBackend

Length of output: 1932


🏁 Script executed:

# Check reliabilityAlertQueue.js around line 109 to see how events are structured
sed -n '100,120p' packages/common/src/queues/reliabilityAlertQueue.js

Repository: geturbackend/urBackend

Length of output: 560


🏁 Script executed:

# Check both emitEvent.js files to see full context
cat apps/public-api/src/utils/emitEvent.js

Repository: geturbackend/urBackend

Length of output: 748


TTL index correctly expires documents 730 days from the timestamp field.

The schema has default: Date.now for timestamp, and current code (in emitEvent.js and reliabilityAlertQueue.js) explicitly sets timestamp: new Date(), ensuring documents expire based on creation time. The TTL behavior is working as intended. Note that if timestamp is ever explicitly set to a past or future date in the future, expiry would be calculated from that value, not insertion time—so be cautious when overriding the timestamp field.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/common/src/models/PlatformEvent.js` around lines 55 - 58, The TTL
index on platformEventSchema is correct but we should add a clarifying comment
and safeguard to prevent accidental expiry surprises: update the
PlatformEvent.js near platformEventSchema.index to document that
expireAfterSeconds is calculated from the document's timestamp field value
(which defaults to Date.now) and warn that any explicit timestamp overrides
(e.g., in emitEvent.js and reliabilityAlertQueue.js where timestamp: new Date()
is set) will affect expiry; also ensure the schema keeps default: Date.now and
consider adding a unit test or runtime assertion in
emitEvent.js/reliabilityAlertQueue.js that timestamp is not set to a past date
before insert.

Comment thread packages/common/src/queues/activityRollupQueue.js Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a first-party analytics/metrics stack (event logging + rollups) to track activation, engagement/retention, and platform reliability, and surfaces the results in both the developer dashboard and an admin/operator metrics page.

Changes:

  • Introduces new Mongo models (PlatformEvent, DeveloperActivity) plus BullMQ queues for daily activity rollups and frequent reliability spike detection.
  • Adds instrumentation + new analytics/admin endpoints in dashboard-api, and emits first_api_success from public-api middleware.
  • Adds new React UI for per-developer metrics and an admin “Platform Metrics” page.

Reviewed changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 16 comments.

Show a summary per file
File Description
packages/common/src/queues/reliabilityAlertQueue.js Adds a BullMQ repeatable job + worker to detect error-rate spikes and write reliability_spike events.
packages/common/src/queues/activityRollupQueue.js Adds a daily BullMQ rollup job to aggregate Log data into DeveloperActivity.
packages/common/src/models/PlatformEvent.js Introduces a TTL’d event collection with indexes for funnel queries.
packages/common/src/models/index.js Expands model exports for queue modules to import from a single index.
packages/common/src/models/DeveloperActivity.js Introduces a per-developer per-day rollup schema (unique index on developerId+date).
packages/common/src/index.js Exports the new models and queues from @urbackend/common.
apps/web-dashboard/src/pages/Dashboard.jsx Adds the new per-developer metrics card to the main dashboard page.
apps/web-dashboard/src/pages/AdminMetrics.jsx Adds a new operator/admin metrics page with overview, funnel, cohorts, reliability, top projects, and churn.
apps/web-dashboard/src/index.css Adds styling for the new Admin Metrics page.
apps/web-dashboard/src/components/Dashboard/DeveloperMetrics.jsx Adds a “My Performance” component that calls new analytics endpoints and renders activation + 30-day engagement.
apps/web-dashboard/src/App.jsx Registers a new /admin/metrics route in the web dashboard.
apps/public-api/src/utils/emitEvent.js Adds a fire-and-forget helper for writing PlatformEvent from public-api.
apps/public-api/src/middlewares/api_usage.js Emits first_api_success once per project using a Redis NX flag.
apps/public-api/src/app.js Initializes and schedules the new BullMQ workers/cron jobs in public-api startup.
apps/dashboard-api/src/utils/emitEvent.js Adds a fire-and-forget helper for writing PlatformEvent from dashboard-api.
apps/dashboard-api/src/routes/events.js Adds an endpoint for dashboard UI → backend event tracking.
apps/dashboard-api/src/routes/analytics.js Adds developer-facing metrics endpoints (funnel/retention/engagement/north-star).
apps/dashboard-api/src/routes/admin.metrics.js Adds admin-only metrics endpoints under /api/admin/metrics/*.
apps/dashboard-api/src/controllers/project.controller.js Emits project_created / collection_created activation events on successful commit.
apps/dashboard-api/src/controllers/events.controller.js Implements frontend event allowlist + normalization and forwards to emitEvent.
apps/dashboard-api/src/controllers/auth.controller.js Emits signup_completed and email_verified activation events.
apps/dashboard-api/src/controllers/analytics.controller.js Implements new funnel/retention/engagement/north-star endpoints.
apps/dashboard-api/src/controllers/admin.metrics.controller.js Implements admin overview/funnel/cohorts/usage/reliability/top-projects/churn queries.
apps/dashboard-api/src/app.js Wires the new events and admin metrics routes.
apps/dashboard-api/src/tests/auth.controller.test.js Updates mocks to accommodate new PlatformEvent.create() usage in auth flows.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +24 to +32
await activityRollupQueue.add(
'daily-rollup',
{},
{
repeat: { cron: '5 0 * * *' }, // 00:05 UTC daily
removeOnComplete: true,
removeOnFail: { count: 10 },
},
);
@@ -0,0 +1,161 @@
const { Queue, Worker } = require('bullmq');
const connection = require('../config/redis');
const mongoose = require('mongoose');
Comment on lines +100 to +117
const ownerId = ownerMap[row._id.toString()];
if (!ownerId) continue;
const key = ownerId.toString();
if (!devMap[key]) {
devMap[key] = {
developerId: ownerId,
activeProjectIds: [],
apiCallCount: 0,
mailSentCount: 0,
storageUploadsCount: 0,
webhookTriggeredCount: 0,
};
}
devMap[key].activeProjectIds.push(row._id);
devMap[key].apiCallCount += row.apiCallCount;
devMap[key].mailSentCount += row.mailCount;
devMap[key].storageUploadsCount += row.storageCount;
}
Comment on lines +63 to +66
// 1. Aggregate logs by project for the day
const logAgg = await Log.aggregate([
{ $match: { timestamp: { $gte: dayStart, $lt: dayEnd } } },
{
/**
* Run the reliability check.
* Looks at the last 15 minutes of ApiAnalytics.
* If a project has >50 total requests and >5% error rate (5xx or 4xx depending on preference, we'll use >= 500 for true platform errors),
Comment on lines +261 to +272
return res.json({
success: true,
data: {
window: '30d',
totalApiCalls: result.totalApiCalls,
totalMailSent: result.totalMailSent,
totalStorageUploads: result.totalStorageUploads,
totalWebhooksFired: result.totalWebhooksFired,
activeDays: result.activeDays,
uniqueActiveProjects,
},
message: '',
Comment on lines +74 to +83
// --- Activation funnel: first_api_success ---
// Fires only once per project lifetime, on the very first 2xx response.
// Uses a permanent Redis NX flag so we don't hit MongoDB on every request.
if (req.project && res.statusCode >= 200 && res.statusCode < 300) {
setImmediate(async () => {
try {
const flagKey = `project:activation:first_api_success:${req.project._id}`;
const isFirst = await redis.set(flagKey, '1', 'NX');
if (isFirst) {
const { Project, PlatformEvent } = require('@urbackend/common');
Comment thread apps/public-api/src/utils/emitEvent.js Outdated
@@ -0,0 +1,25 @@
const { PlatformEvent, Project } = require('@urbackend/common');
Comment thread apps/public-api/src/app.js Outdated
Comment on lines +26 to +41
const {initActivityRollupWorker, scheduleActivityRollup} = require('@urbackend/common');
const {initReliabilityAlertWorker, scheduleReliabilityAlert} = require('@urbackend/common');

// Initialize webhook worker
if (process.env.NODE_ENV !== 'test') {
initWebhookWorker();
initAuthEmailWorker();
initPublicEmailWorker();
initActivityRollupWorker();
scheduleActivityRollup().catch((err) =>
console.error('[ActivityRollup] Failed to schedule cron:', err.message)
);
initReliabilityAlertWorker();
scheduleReliabilityAlert().catch((err) =>
console.error('[ReliabilityAlert] Failed to schedule cron:', err.message)
);
Comment on lines +33 to +42
] = await Promise.all([
Developer.countDocuments(),
Developer.countDocuments({ isVerified: true }),
Project.countDocuments(),
Log.countDocuments(),
Log.distinct('projectId', {
status: { $gte: 200, $lt: 300 },
timestamp: { $gte: sevenDaysAgo },
}),
]);
@yash-pouranik

Copy link
Copy Markdown
Member Author

@copilot apply changes based on the comments in this PR

Agent-Logs-Url: https://github.com/geturbackend/urBackend/sessions/97646711-efe2-4fe4-a00f-1a81c8dfd698

Co-authored-by: yash-pouranik <172860064+yash-pouranik@users.noreply.github.com>

Copilot AI commented May 11, 2026

Copy link
Copy Markdown
Contributor

@copilot apply changes based on the comments in this PR

Implemented the requested PR feedback in commit 115d032 (with the main batch in 9ff6e73). This includes BullMQ repeat scheduling fix, auth protection on events tracking, cohort/query scalability fixes, endpoint payload alignment, first_api_success coverage+TTL, and worker bootstrap ordering after DB connect.

UI screenshot (DeveloperMetrics field alignment update): /tmp/developer-metrics-ui.png

@yash-pouranik

Copy link
Copy Markdown
Member Author

@coderabbitai

ok to merge??

@coderabbitai

coderabbitai Bot commented May 11, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get PR status, reviews, checks, and open comments
gh pr view 165 --repo geturbackend/urBackend --json state,mergeable,mergeStateStatus,reviewDecision,reviews,statusCheckRollup,comments

echo "--- REVIEW COMMENTS ---"
gh api repos/geturbackend/urBackend/pulls/165/comments --jq '[.[] | {id: .id, user: .user.login, body: .body, resolved: .resolved}]'

echo "--- PR REVIEWS ---"
gh api repos/geturbackend/urBackend/pulls/165/reviews --jq '[.[] | {id: .id, user: .user.login, state: .state, body: .body}]'

echo "--- CHECK RUNS ---"
gh api repos/geturbackend/urBackend/commits/$(gh pr view 165 --repo geturbackend/urBackend --json headRefOid --jq '.headRefOid')/check-runs --jq '[.check_runs[] | {name: .name, status: .status, conclusion: .conclusion}]' 2>/dev/null || echo "No check runs found"

Length of output: 204939


@yash-pouranikNot quite yet. The PR is in good shape overall, but there are several issues that should be addressed before merging:


🔴 Blocking Issues

# Issue Location
1 Security — raw err.message leaked to clients in all catch blocks admin.metrics.controller.js, analytics.controller.js, events.controller.js
2 Functional bug — events.js route missing authMiddleware — only verifyEmail is applied, so unauthenticated requests will throw a TypeError and return a 500 apps/dashboard-api/src/routes/events.js
3 UI bug — field name mismatch — backend getEngagement returns totalApiCalls, totalMailSent, totalStorageUploads, totalWebhooksFired but DeveloperMetrics.jsx reads apiCalls, mailSent, storageUploads, webhooksFired → all engagement stats display as 0 DeveloperMetrics.jsxanalytics.controller.js
4 Race condition — BullMQ workers start before connectDB() — workers can fire Mongo queries on a disconnected connection apps/public-api/src/app.js
5 Data integrity — rollup reads from a capped Log collection (50k docs / 50MB cap) — under load, yesterday's logs may be truncated before the 00:05 UTC rollup, silently producing incorrect retention/engagement metrics activityRollupQueue.js

🟡 Should-Fix Before Merge

  • Admin guard only in controller, not at router level — a future endpoint addition will silently bypass the check (admin.metrics.js routes)
  • API contract violations — some 400 responses in events.controller.js are missing the data: {} field
  • Unused imports causing lint riskActivity in DeveloperMetrics.jsx, Project in public-api/src/utils/emitEvent.js, mongoose in queue files

ℹ️ Pre-Merge Check Failures

  • Docstring coverage is 73.33%, below the 80% threshold
  • PR title is flagged as vague ("Platform metric")

✅ What's Already Good

  • All CI tests pass (dashboard-api, public-api, sdk, frontend-lint, CodeQL)
  • Copilot addressed the N+1 retention queries, first_api_success Redis TTL, and webhook count in commit 9ff6e73
  • Architecture is solid — fire-and-forget emitEvent, async BullMQ rollups, Redis NX dedup

Once items 1–5 are addressed (especially the auth gap on /api/events/track and the engagement field name mismatch), this should be good to go. Would you like me to trigger Copilot to fix these remaining issues?

@yash-pouranik

Copy link
Copy Markdown
Member Author

@copilot
Please read the last comment by coderabbitai and fix the changes needed

Agent-Logs-Url: https://github.com/geturbackend/urBackend/sessions/9df9708d-0389-45cb-97ef-0f88fa9147fc

Co-authored-by: yash-pouranik <172860064+yash-pouranik@users.noreply.github.com>
@yash-pouranik
yash-pouranik temporarily deployed to feat/metrics-stack - urBackend-frankfrut PR #165 May 11, 2026 20:48 — with Render Destroyed

Copilot AI commented May 11, 2026

Copy link
Copy Markdown
Contributor

@copilot
Please read the last comment by coderabbitai and fix the changes needed

Done in ac4eef0. I applied the remaining fixes from the latest CodeRabbit note: sanitized error responses in admin.metrics.controller.js and analytics.controller.js (no raw err.message leakage), and aligned events.controller.js error payloads with the API contract (success, data, message). I also re-checked workflows/logs and reran dashboard-api tests; validation is clean (Code Review + CodeQL).

@yash-pouranik

Copy link
Copy Markdown
Member Author

please check now

@coderabbitai

@coderabbitai

coderabbitai Bot commented May 11, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get latest PR state including recent commits and check runs
gh pr view 165 --repo geturbackend/urBackend --json state,mergeable,mergeStateStatus,reviewDecision,commits,statusCheckRollup

echo "=== RECENT COMMITS ==="
gh api repos/geturbackend/urBackend/pulls/165/commits --jq '[.[] | {sha: .sha[:8], message: .commit.message, date: .commit.author.date}]'

echo "=== OPEN REVIEW COMMENTS ==="
gh api repos/geturbackend/urBackend/pulls/165/comments --jq '[.[] | {id: .id, user: .user.login, path: .path, body: .body[:200]}]'

Length of output: 16674


🏁 Script executed:

#!/bin/bash
echo "=== events.js route ==="
gh api repos/geturbackend/urBackend/contents/apps/dashboard-api/src/routes/events.js?ref=feat/metrics-stack --jq '.content' | base64 -d

echo ""
echo "=== DeveloperMetrics.jsx ==="
gh api repos/geturbackend/urBackend/contents/apps/web-dashboard/src/components/Dashboard/DeveloperMetrics.jsx?ref=feat/metrics-stack --jq '.content' | base64 -d

echo ""
echo "=== analytics.controller.js (catch blocks) ==="
gh api repos/geturbackend/urBackend/contents/apps/dashboard-api/src/controllers/analytics.controller.js?ref=feat/metrics-stack --jq '.content' | base64 -d

echo ""
echo "=== admin.metrics.controller.js (catch blocks, first 60 lines) ==="
gh api repos/geturbackend/urBackend/contents/apps/dashboard-api/src/controllers/admin.metrics.controller.js?ref=feat/metrics-stack --jq '.content' | base64 -d | head -80

echo ""
echo "=== public-api app.js (worker init section) ==="
gh api repos/geturbackend/urBackend/contents/apps/public-api/src/app.js?ref=feat/metrics-stack --jq '.content' | base64 -d

Length of output: 23547


🏁 Script executed:

#!/bin/bash
echo "=== events.controller.js ==="
gh api repos/geturbackend/urBackend/contents/apps/dashboard-api/src/controllers/events.controller.js?ref=feat/metrics-stack --jq '.content' | base64 -d

echo ""
echo "=== activityRollupQueue.js ==="
gh api repos/geturbackend/urBackend/contents/packages/common/src/queues/activityRollupQueue.js?ref=feat/metrics-stack --jq '.content' | base64 -d

Length of output: 6358


@yash-pouranik — Great work! I've re-inspected all 5 previously blocking issues and they are all resolved. Here's the full breakdown:


✅ All 5 Blocking Issues Fixed

# Issue Status
1 err.message leaked to clients ✅ Fixed — all catch blocks in analytics.controller.js, admin.metrics.controller.js, and events.controller.js now return 'Internal server error'
2 events.js missing authMiddleware ✅ Fixed — route is now router.post('/track', authMiddleware, verifyEmail, track)
3 Field name mismatch (apiCalls vs totalApiCalls) ✅ Fixed — DeveloperMetrics.jsx now reads totalApiCalls, totalMailSent, totalStorageUploads, totalWebhooksFired to match the backend
4 BullMQ workers starting before connectDB() ✅ Fixed — bootstrap() now await connectDB() before calling startWorkers()
5 Rollup reading from capped Log collection ✅ Fixed — activityRollupQueue.js now aggregates from ApiAnalytics (unbounded collection) instead of the capped Log collection

✅ Other Previously Flagged Items Also Fixed

  • BullMQ v5 cron syntax → now correctly uses repeat: { pattern: '5 0 * * *' }
  • webhookTriggeredCount always 0 → now correctly summed via regex match on /api/webhooks?/
  • Unused Activity import in DeveloperMetrics.jsx → removed
  • Unused mongoose import in activityRollupQueue.js → removed
  • All 400 responses in events.controller.js now include data: {}

🟡 Remaining Minor Open Items (Non-Blocking)

These won't break functionality but are worth a follow-up PR:

  • admin.metrics.controller.js: cohortSize can be inflated by duplicate signup_completed events; N+1 queries in cohort retention; $addToSet memory risk on large datasets
  • admin.metrics.js router: Admin guard is only defense-in-depth in the controller, not applied at the router level
  • reliabilityAlertQueue.js: Doc comment says >50 requests but code threshold is >= 20

This PR is good to merge. All CI checks pass and all blocking issues are resolved. 🚀

[approve]

@yash-pouranik

Copy link
Copy Markdown
Member Author

please create one follow up issue @coderabbitai

@coderabbitai

coderabbitai Bot commented May 11, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

Sure! Creating the follow-up issue now.


🏁 Script executed:

#!/bin/bash
gh issue create \
  --repo geturbackend/urBackend \
  --title "follow-up(analytics): Minor issues from PR `#165` metrics stack" \
  --assignee yash-pouranik \
  --body "## Follow-up items from PR `#165` (feat/metrics-stack)

These items were identified during the code review of [PR `#165`](https://github.com/geturbackend/urBackend/pull/165) but are non-blocking and deferred to a follow-up.

Requested by `@yash-pouranik`.

---

### 1. \`admin.metrics.controller.js\` — Cohort accuracy & memory

- **Duplicate signup events inflate cohortSize**: \`getCohorts\` counts all \`signup_completed\` events for a given month but a developer can emit that event more than once (e.g. multiple OAuth logins), causing the cohort denominator to be inflated. Deduplicate by \`developerId\` before counting.
- **N+1 queries in cohort retention**: The retention check loops per cohort member and issues individual \`DeveloperActivity.findOne\` calls. Replace with a single \`\$in\` query for all developer IDs and resolve retention in memory.
- **\`\$addToSet\` memory risk on large datasets**: The feature-usage aggregation uses \`\$addToSet: '\$developerId'\` in-memory in the aggregation pipeline, which can exceed the 100 MB aggregation memory limit for large collections. Consider using \`\$count\` with a \`\$group\` stage or a \`\$facet\` approach instead.

---

### 2. \`apps/dashboard-api/src/routes/admin.metrics.js\` — Admin guard at router level

The admin authorization check (\`requireAdmin\`) is applied inside each controller handler rather than as route-level middleware. A future endpoint added to this router could silently bypass the guard. Move \`requireAdmin\` (or an equivalent middleware) to the router level so it applies automatically to all routes under \`/api/admin/metrics\`.

---

### 3. \`packages/common/src/queues/reliabilityAlertQueue.js\` — Doc comment mismatch

The JSDoc/inline comment describing the error-rate spike threshold states \`>50 requests\` but the actual code guard is \`>= 20\` requests. Update the comment to match the real threshold to avoid confusion for future maintainers.

---

_Backlinked from PR `#165` comment: https://github.com/geturbackend/urBackend/pull/165_"

Length of output: 53


@yash-pouranik — Done! Follow-up issue created and assigned to you: #166 — follow-up(analytics): Minor issues from PR #165 metrics stack

It covers the three remaining items:

  1. Cohort accuracy & memory (admin.metrics.controller.js) — duplicate signup inflation, N+1 retention queries, $addToSet memory risk
  2. Admin guard at router level (admin.metrics.js) — move requireAdmin to middleware on the router
  3. Doc comment mismatch (reliabilityAlertQueue.js) — >50 requests comment vs >= 20 in code

@yash-pouranik
yash-pouranik merged commit e937300 into main May 11, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants